package auction.server;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Timer;
import org.apache.log4j.Logger;

import java.security.NoSuchAlgorithmException;
import javax.crypto.NoSuchPaddingException;

import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.PrivateKey; 
import java.security.PublicKey;
import java.security.Security;
import java.security.Signature;
import java.security.SignatureException;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMReader; 
import org.bouncycastle.openssl.PasswordFinder;
import org.bouncycastle.util.encoders.Base64;

import auction.domain.Auction;

import auction.domain.User;
import auction.synchronisation.Groupbid;

public class Server {
	public static Logger log = Logger.getLogger(Server.class);

	private static ArrayList<ClientThread> clients = new ArrayList<ClientThread>();
	private static ArrayList<User> users = new ArrayList<User>();
	private static ArrayList<Auction> auctions = new ArrayList<Auction>();
	private static ArrayList<Groupbid> groupbids = new ArrayList<Groupbid>();
	private static int id = 0;
	private static boolean resume;

	private static ServerSocket serverSocket;
	private static Socket socket;

	private static BufferedReader inReadConsole;

	static Timer timer = new Timer();

	private static String clientkeydir;
	private static String serverKey;
	private static char[] password;
	private static PrivateKey privateKey;

	private static boolean serverUnavailable = false;
	private static int port ;

	/*
	 * main
	 */
	public static void main(String[] args) {
		/* check arguments */

		//Assignment 3:
		/*  <arg value="${server.port}"/>
            <arg value="${analytics.bindingName}"/>
            <arg value="${billing.bindingName}"/>
            <arg value="${server.key}"/>
            <arg value="${clients.key.dir}"/>
		 */
		boolean correctArgs = args.length == 5 && args[0].matches("[0-9]+");
		port = 0;

		if (correctArgs) {
			port = Integer.parseInt(args[0]);
			serverKey = args[3];
			clientkeydir = args[4];
			correctArgs = correctArgs && port > 0 && port < 65536;
		}
		else {
			System.err.println("Usage: java auction.server.Server <server port> <analytics.bindingName> <billing.bindingName> <server.key> <clients.key.dir>");
			System.exit(1);
		}

		serverSocket = null;
		try {
			serverSocket = new ServerSocket(port);

		} catch (IOException e) {
			System.out.println("Could not bind to port " + port + " - "
					+ e.getMessage());
			System.exit(1);
		}
		resume=true;

		//gemeinsame konsole fuer passphrase und shutdwonthread
		inReadConsole=new BufferedReader(new InputStreamReader(System.in));

		Security.addProvider(new BouncyCastleProvider());
		if(!readRSAPrivateKey())
			resume = false;

		//Info: reading PublicKey later in ClientThread

		ShutdownThread t = new ShutdownThread();
		t.start();

		while (resume) {

			if(!Server.serverUnavailable){
				try {

					socket = serverSocket.accept();
				}catch (IOException e) {
					log.debug("Could not listen for incoming connections - "
							+ e.getMessage());

					Server.serverUnavailable = true;
				}
			}

			if(!Server.serverUnavailable){

				ClientThread thread;
				try {
					thread = new ClientThread(socket);
					synchronized (clients) {
						clients.add(thread);
					}
					thread.start();

				} catch (IOException e1) {
					System.err.println("Incoming connection error - "
							+ e1.getMessage());
					try {
						socket.close();
					} catch (IOException e2) {
						/* ignore */
					}
				} catch (NoSuchAlgorithmException e1) {
					System.err.println("Incoming connection error - "
							+ e1.getMessage());
					try {
						socket.close();
					} catch (IOException e2) {
						/* ignore */
					}
				} catch (NoSuchPaddingException e1) {
					System.err.println("Incoming connection error - "
							+ e1.getMessage());
					try {
						socket.close();
					} catch (IOException e2) {
						/* ignore */
					}
				}
			}
		}

		//server shutdown
		try {			
			serverSocket.close();		
		}
		catch (IOException e) {
			log.debug(e.getMessage());
		}
		System.exit(0);

	}

	public static void reconnectServerSocket(){
		try {
			serverSocket = new ServerSocket(port);
			Server.serverUnavailable = false;

		} catch (IOException e) {
			System.out.println("Could not bind to port " + port + " - "
					+ e.getMessage());
			System.exit(1);
		}
	}
	public static void closeServerSocket(){
		synchronized (clients) {		        		
			for (ClientThread ct : clients) {	        					
				ct.setStop(true);
				ct.closeSocket();
			}

		}
		try {

			serverSocket.close();
		} catch (IOException e) {
			/*ignore*/
		}
	}

	public static String getClientKeyDir(){
		return clientkeydir;
	}

	public static void shutdown() {
		//stop test clients
		synchronized (clients) {		        		
			for (ClientThread ct : clients) {	        					
				ct.setStop(true);
				ct.closeSocket();
			}
			for(Groupbid gp: groupbids){
				gp.shutdown();
			}
		}
		try {

			serverSocket.close();
		} catch (IOException e) {
			/*ignore*/
		}


	}
	//on exit send all clients !end command
	public static void endClients() {
		synchronized (clients) {
			for (ClientThread ct : clients) {
				ct.shutdown();

			}
		}
	}

	/*
	 * if the client disconnects, the clientthread is removed from the clients list
	 */
	public static void removeClient(ClientThread thread) {
		synchronized (clients) {
			System.out.println("Client disconnected.");
			clients.remove(thread);
		}
	}
	/*
	 * if a new user logged in, the username is add to the users list
	 */
	public static void addUser(User user) {
		synchronized (users) {
			System.out.println("New User " + user.getUsername()
					+ " added.");
			users.add(user);
		}
	}
	/*
	 * if a user is already in the users list, only the boolean loggedIn is updated to true
	 */
	public static void setLoggedIn(String username) {
		synchronized (users) {
			for (User u : users) {

				if (u.getUsername().equals(username) ) {
					u.setLoggedIn(true);
					System.out.println("User "+username+" logged in.");
				}
			}
		}

	}
	/*
	 * if the user logs out, the boolean loggedIn needs to be set false
	 */
	public static void logoutUser(String username) {
		synchronized (users) {
			System.out.println("User "+username+" logged out.");

			for(User u : users){
				if (u.getUsername().equals(username)){
					u.setLoggedIn(false);
				}
			}
		}		
	}
	/*
	 * creates an auction by adding the action in the auctions list and schedule a timer with the specified time for the auction to last
	 */
	public static void createAuction(Auction auction, int time) {
		synchronized (auctions) {
			auctions.add(auction);
			timer.schedule(new AuctionTask(auction), time);
		}
	}
	/*
	 * bidding on an auction
	 */
	public static String bidAuction(String bidder, int id, double amount) {
		String ret="";
		synchronized (auctions) {
			for (Auction a : auctions) {

				if (a.getId()==id && !a.isEnded()) {
					if(amount>a.getHighestBid()){
						a.setHighestBid(amount);
						a.setHighestBidder(bidder);						
					}					
					ret=a.getHighestBidder()+";"+a.getHighestBid()+";"+a.getDescription();					
				}
				else if (a.getId()==id && a.isEnded())
					ret="ended";
			}
		}
		return ret;
	}
	/*
	 * bidding on an auction, through a signedBid (therefore auction can already be ended
	 */
	public static String bidAuctionFromSignedBid(String bidder, int id, double amount, String client1Timestamp, String client2Timestamp)  {

		String ret="";

		long timestamp;


		if(splitClientTimestamp(client1Timestamp)!=null && splitClientTimestamp(client2Timestamp)!=null){
			//arethmetic mean of both timestamps
			timestamp = (Long.parseLong(splitClientTimestamp(client1Timestamp))+Long.parseLong(splitClientTimestamp(client2Timestamp)))/2;
			ret="";
			synchronized (auctions) {
				for (Auction a : auctions) {

					//get end
					Date bidDate = new Date(timestamp);

					SimpleDateFormat formater = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss z");						   
					log.debug("Bid-Zeit: "+formater.format(bidDate));

					Date endDate = null;
					try {
						endDate = formater.parse(a.getEnd());
						log.debug("End-Zeit: "+formater.format(endDate));

						if (a.getId()==id && (bidDate.compareTo(endDate)<0)) {
							if(amount>a.getHighestBid()){
								a.setHighestBid(amount);
								a.setHighestBidder(bidder);		
								a.setBidTimestamp(bidDate);
							}					
							if(amount==a.getHighestBid() && a.getBidTimestamp()!=null){
								if((bidDate.compareTo(a.getBidTimestamp())<0)){
									a.setHighestBid(amount);
									a.setHighestBidder(bidder);		
									a.setBidTimestamp(bidDate);
								}
							}
							ret=a.getHighestBidder()+";"+a.getHighestBid()+";"+a.getDescription();					
						}
						else if (a.getId()==id && (bidDate.compareTo(endDate)>=0))
							ret="ended";

					} catch (ParseException e) {
						log.debug(e.getMessage());
					}
				}
			}

		}
		else
		{
			log.debug("signedBid verifying not sucessfull!");
			ret="fail";
		}



		return ret;
	}
	public static String splitClientTimestamp(String clientTimestamp){
		boolean ok = false;
		String[] s = clientTimestamp.split(":");

		String signingUser = s[0];
		String timestamp = s[1];
		String signature = s[2];
		String auctionId = s[3];
		String price = s[4];

		String dataToVerify = "!timestamp "+auctionId+" "+price+" "+timestamp;
		log.debug("string to verify: "+dataToVerify);

		//verify originalString with signature
		try {
			ok=Server.verifyTimeStampWithSignature(dataToVerify, signature, signingUser);
		} catch (InvalidKeyException e) {
			log.debug(e.getMessage());
		} catch (SignatureException e) {
			log.debug(e.getMessage());
		} catch (NoSuchAlgorithmException e) {
			log.debug(e.getMessage());
		}
		if(ok)
			return timestamp;
		else
			return null;//if verification was not sussessfull
	}
	//Test verify timestamp with signature
	public static boolean verifyTimeStampWithSignature(String dataToVerify, String base64encodedSignature, String signingUser) throws SignatureException, InvalidKeyException, NoSuchAlgorithmException{
		// verify
		Signature instance = Signature.getInstance("SHA512withRSA");
		instance.initVerify(readPublicKeyOfSigningUser(signingUser));
		instance.update(dataToVerify.getBytes()); // der String, der signiert wurde

		byte[] signature = Base64.decode(base64encodedSignature.getBytes());//decode Base64
		return instance.verify(signature); // sig = das byte[] Resultat von oben
	}

	//read publickey of the user who signed the timestamp
	private static PublicKey readPublicKeyOfSigningUser(String signingUser){
		PublicKey publicKey = null;
		String pathToPublicKey = clientkeydir+signingUser+".pub.pem";
		log.debug("Path to PublicKey: "+pathToPublicKey);
		PEMReader in = null;
		try {
			in = new PEMReader(new FileReader(pathToPublicKey));
		} catch (FileNotFoundException e) {
			System.out.println("No PublicKey File found for user "+signingUser);


		} 
		try {
			publicKey = (PublicKey) in.readObject();
		} catch (IOException e) {
			System.out.println("Error: Reading PublicKey File for user "+signingUser);

		}

		try {
			in.close();
		} catch (IOException e) {
			/*ignore*/
		}
		return publicKey;
	}

	/*
	 * groupBid
	 */
	public static String bidAuction(String bidder, int id, double amount,boolean groupb) {
		String ret="";
		synchronized (auctions) {
			for (Auction a : auctions) {

				if (a.getId()==id && !a.isEnded()) {
					if(amount>a.getHighestBid()){
						a.setHighestBid(amount);
						a.setHighestBidder(bidder);
						a.setGroupbidAuction();
					}					
					ret=a.getHighestBidder()+";"+a.getHighestBid()+";"+a.getDescription();					
				}
				else if (a.getId()==id && a.isEnded())
					ret="ended";
			}
		}
		return ret;
	}

	/*
	 * generates the next auctionId
	 */
	public static int getNextAuctionId() {
		return id+=1;
	}
	/*
	 * if an auction is ended, the boolean ended from the auction has to be set true
	 */
	public static void endAuction(Auction auction) {
		auction.setEnded(true);
		synchronized (groupbids) {
			for (Groupbid gb : groupbids) {

				if (auction.getId()==gb.getBidId()) {
					if(groupbids.contains(gb)){
						groupbids.remove(gb);
						break;
					}
				}
			}

		}
	}

	/*
	 * lists all active auctions
	 */
	public static String getAuctionList() {
		String ret = "";

		synchronized (auctions) {
			for (Auction auction : auctions) {
				if(!auction.isEnded())
					ret += auction.toString() + ";";
			}
		}

		if(ret.length()>1)
			return ret.substring(0, ret.length() - 1);
		else
			return ret;
	}

	/*
	 * returns true if the username is already in the userslist
	 */
	public static boolean usernameExists(String name) {
		synchronized (users) {
			for (User u : users) {

				if (u.getUsername().equals(name)) {
					return true;
				}
			}
		}
		return false;
	}

	/*
	 * returns true if the user is logged in
	 */
	public static boolean userLoggedIn(String username) {
		synchronized (users) {
			for (User u : users) {

				if (u.getUsername().equals(username) && u.isLoggedIn()) {
					return true;
				}
			}
		}
		return false;
	}

	//_________________Stage 1______________________________________________________

	//read privateKey of auction server
	private static boolean readRSAPrivateKey(){
		boolean ok = true;
		privateKey = null;
		String pathToPrivateKey = serverKey;
		log.debug("Path to PrivateKey: "+pathToPrivateKey);
		PEMReader in = null;
		try {
			in = new PEMReader(new FileReader(pathToPrivateKey), new PasswordFinder() {
				@Override
				public char[] getPassword() {
					// reads the password from standard input for decrypting the private key
					System.out.println("Enter pass phrase:");
					try {
						password = inReadConsole.readLine().toCharArray();
						log.debug("Password: "+new String(password));
						return password;
					} catch (IOException e) {
						System.out.println("Error: Reading Password");
					}
					return null;
				}
			});
		} catch (FileNotFoundException e1) {
			System.out.println("No PEM File found for auction server");
			ok = false;
		}
		KeyPair keyPair;
		try {
			keyPair = (KeyPair) in.readObject();
			privateKey = keyPair.getPrivate();
		} catch (IOException e) {
			System.out.println("Error: Reading file");
			log.debug("Error: Reading file: "+e.getMessage());
			ok = false;
		} 
		try {
			in.close();
		} catch (IOException ex) {
			/*ignore*/
		}

		return ok;
	}


	//_________________Stage 3_______________________________________________________

	//Changed{
	public synchronized static String groupbidAuction(ClientThread ct, int id, double amount) {
		synchronized (auctions) {
			for (Auction a : auctions) {



				if (a.getId()==id && !a.isEnded()) {
					if((!Server.isGroupbidAuction(id))&&(!a.isGroupbidAuction())){
						int ccth = Server.countUsersessions();
						int cgba = Server.countGroupbitAuctions();
						if(ccth>cgba){
							if((ccth-cgba)==1){
								int p = ct.getBidPriority();
								ClientThread higherClient = getHighPriority(p);
								if(!(higherClient==null)){
									higherClient.decrBidPriority();
									return "reject";
								}

							}
						}
						else{
							return "reject"; 
						}
					}
					Groupbid gp = new Groupbid (ct.getUsername(), id, amount, groupbids);
					gp.start();
					groupbids.add(gp);		
					return "placed";					
				}
				else if (a.getId()==id && a.isEnded())
					return "ended";
			}
		}
		return "";
	}
	public static ClientThread getHighPriority(int cp){
		int p=cp;
		ClientThread rc=null;
		for(ClientThread c: clients){
			if(p<c.getBidPriority()){
				p=c.getBidPriority();
				rc =c;
			}
		}
		return rc;
	}
	//Changed}
	public static Groupbid getGroupbit(String bidder, int id, double amount){
		for(Groupbid gp:groupbids){
			if(gp.getBidId()==id){
				if(gp.getBidder().equals(bidder)){
					if(gp.getAmaount()==amount){
						return gp;
					}
				}
			}
		}
		return null;
	}
	public static boolean isConfirmed(Groupbid gp){
		if(groupbids.contains(gp)){
			return gp.isConfirmed();
		}
		return false;

	}
	public static boolean confirm(Groupbid gp){
		if(groupbids.contains(gp)){
			if (gp.isConfirmed()){
				return false;
			}
			else{
				gp.confirm();
				return true;
			}
		}
		return false;
	}
	public static String getGroupbitAnswer(Groupbid gp){
		if(groupbids.contains(gp)){
			return gp.getbidAnswer();	
		}
		return "";
	}
	public static Integer countUsersessions(){

		int count = 0;
		synchronized(users){
			for(User u:users){
				if(u.isLoggedIn()){
					count ++;
				}
			}
		}
		return count;
	}
	public static boolean isGroupbidAuction(int id){
		synchronized(groupbids){
			for(Groupbid gp:groupbids){
				if(gp.getBidId()==id){
					return true;
				}
			}
		}
		return false;
	}
	public static Integer countGroupbitAuctions(){
		ArrayList<Integer> ids = new ArrayList<Integer>();
		synchronized(groupbids){
			for(Groupbid gp:groupbids){
				if(!ids.contains(gp.getBidId())){
					ids.add(gp.getBidId());
				}
			}
		}
		synchronized (auctions){
			for (Auction a: auctions){
				if(a.isGroupbidAuction()&&(!a.isEnded())){
					if(!ids.contains(a.getId())){
						ids.add(a.getId());
					}
				}
			}
		}
		log.debug("Number of active groupbid auctions is: "+ids.size());
		return ids.size();
	}


	//_________________Stage 4_______________________________________________________

	public static String getClientList(){
		String ret = "";

		synchronized (clients) {
			for (ClientThread client : clients) {
				if(!client.isStop() && client.getUsername()!=null)
					ret += client.getClientHostAddress()+":"+client.getClientTCPPort()+" - "+client.getUsername() + ";";
			}
		}

		if(ret.length()>1){
			ret = "Active Clients:"+";"+ret;
			return ret.substring(0, ret.length() - 1);
		}
		else
			return ret;
	}

	//on login of any user send new Clientlist to all Clients
	public static void sendCurrentClientListToAllClients(){
		String clientList = getClientList();
		synchronized (clients) {
			for (ClientThread client : clients) {
				if(client.getUsername()!=null)
					client.sendClientList(clientList);	
			}
		}
	}

	//_______________________________________________________________________________


	public static boolean isResume() {
		return resume;
	}

	public static void setResume(boolean resume) {
		Server.resume = resume;
	}
	public static BufferedReader getInReadConsole() {
		return inReadConsole;
	}
	public static void setInReadConsole(BufferedReader inReadConsole) {
		Server.inReadConsole = inReadConsole;
	}
	public static PrivateKey getPrivateKey() {
		return privateKey;
	}
	public static void setPrivateKey(PrivateKey privateKey) {
		Server.privateKey = privateKey;
	}
	public static ServerSocket getServerSocket() {
		return serverSocket;
	}
	public static void setServerSocket(ServerSocket serverSocket) {
		Server.serverSocket = serverSocket;
	}
	public static boolean isServerUnavailable() {
		return serverUnavailable;
	}
	public static void setServerUnavailable(boolean serverUnavailable) {
		Server.serverUnavailable = serverUnavailable;
	}


}
